package prvydevw.manager;

import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.basex.core.BaseXException;
import org.basex.core.Context;
import org.basex.core.cmd.XQuery;
import prvydevw.entity.Payment;
import prvydevw.entity.Subject;

/**
 *
 * @author ivo
 */
public class BasexPaymentManager implements PaymentManager
{
    private Context context;
    private SubjectManager subjectManager;
    
    private static final Logger logger = Logger.getLogger(BasexPaymentManager.class.getName());
    private static final Object INSERT_LOCK = new Object();
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    
    public BasexPaymentManager(Context context)
    {
        this.context = context;
    }
    
    public void setSubjectManager(SubjectManager manager)
    {
        subjectManager = manager;
    }
    
    @Override
    public void createPayment(Payment payment)
    {
        validate(payment);
        
        if (payment.getId() != null) {
            throw new IllegalEntityException("Payment id is already set");
        }
        
        try {
            synchronized(INSERT_LOCK) {
                Long newId = getNextId();
                
                String paymentXml = paymentToXml(payment, newId);
                String query = "insert node " + paymentXml + " into /prvydevw/payments";
                
                new XQuery(query).execute(context);
                
                payment.setId(newId);
            }
        }
        catch (BaseXException e) {
            String msg = "Error when inserting payment into db";
            logger.log(Level.SEVERE, msg, e);
            
            throw new ServiceFailureException(msg, e);
        }
    }

    @Override
    public void updatePayment(Payment payment)
    {
        validate(payment);
        
        if (payment.getId() == null) {
            throw new IllegalEntityException("Payment must have id");
        }
        
        try {
            String paymentXml = paymentToXml(payment, payment.getId());
            String query = "replace node /prvydevw/payments/payment[@id = " + payment.getId() + "] with " + paymentXml;
            
            new XQuery(query).execute(context);
        }
        catch(BaseXException e) {
            String msg = "Error when updating payment";
            logger.log(Level.SEVERE, msg, e);
            
            throw new ServiceFailureException(msg, e);
        }
    }

    @Override
    public void deletePayment(Payment payment)
    {   
        if (payment == null || payment.getId() == null) {
            throw new IllegalEntityException("Payment must have id");
        }
        
        try {
            String selectQuery = "for $payment in /prvydevw/payments/payment[@id = " + payment.getId() + "] return $payment/@id/data()";
            String paymentId = new XQuery(selectQuery).execute(context);
            
            if (paymentId.isEmpty()) {
                throw new IllegalEntityException("Payment " + payment + " does not exists");
            }
            
            String query = "delete node /prvydevw/payments/payment[@id = " + payment.getId() + "]";
            
            new XQuery(query).execute(context);
        }
        catch(BaseXException e) {
            String msg = "Error when deleting payment";
            logger.log(Level.SEVERE, msg, e);
            
            throw new ServiceFailureException(msg, e);
        }
    }

    @Override
    public Payment findPayment(Long id)
    {
        if (id == null) {
            throw new IllegalArgumentException("ID must not be null");
        }
        
        Payment payment = null;
        
        try {
            String data = new XQuery("for $payment in /prvydevw/payments/payment[@id = " + id + "] return $payment/@id/data()").execute(context);
            
            if (data.isEmpty()) {
                return null;
            }
            
            payment = getPayment(id);
        }
        catch (BaseXException e) {
            String msg = "Error when getting payment" + id;
            logger.log(Level.SEVERE, msg, e);
            
            throw new ServiceFailureException(msg, e);
        }
        
        return payment;
    }

    @Override
    public List<Payment> findAllPayments()
    {
        List<Payment> payments = new ArrayList<Payment>();
        
        try {
            String ids = new XQuery("for $payment in /prvydevw/payments/payment return $payment/@id/data()").execute(context);
            
            if (ids.isEmpty()) {
                return payments;
            }
            
            for (String idString: ids.split(" ")) {
                Long id = Long.parseLong(idString);
                
                payments.add(getPayment(id));
            }
        }
        catch (BaseXException e) {
            String msg = "Error when getting all payments";
            logger.log(Level.SEVERE, msg, e);
            
            throw new ServiceFailureException(msg, e);
        }
        
        return payments;
    }
    
    private Payment getPayment(Long id) throws BaseXException
    {
        Payment payment = new Payment();
        
        String subjectIdString = new XQuery("for $payment in /prvydevw/payments/payment[@id = " + id + "] return $payment/subjectId/text()").execute(context);
        String amountString = new XQuery("for $payment in /prvydevw/payments/payment[@id = " + id + "] return $payment/amount/text()").execute(context);
        String number = new XQuery("for $payment in /prvydevw/payments/payment[@id = " + id + "] return $payment/number/text()").execute(context);
        String dateString = new XQuery("for $payment in /prvydevw/payments/payment[@id = " + id + "] return $payment/date/text()").execute(context);
        String description = new XQuery("for $payment in /prvydevw/payments/payment[@id = " + id + "] return $payment/description/text()").execute(context);
        
        Long subjectId;
        Date paymentDate = new Date();
        
        try {
            subjectId = Long.parseLong(subjectIdString);
            paymentDate = DATE_FORMAT.parse(dateString);
        }
        catch (NumberFormatException e) {
            throw new BaseXException("Invalid data received from database", e);
        }
        catch (ParseException ex) {
            throw new BaseXException("Invalid data received from database", ex);
        }
        
        BigDecimal amount = new BigDecimal(amountString);
        Calendar date = Calendar.getInstance();
        date.setTime(paymentDate);
        
        payment.setAmount(amount);
        payment.setDate(date);
        payment.setDescription(description);
        payment.setId(id);
        payment.setSubjectID(subjectId);
        payment.setNumber(number);
        
        return payment;
    }
    
    private Long getNextId() throws BaseXException
    {
        String newIdString = new XQuery("let $id := max(/prvydevw/payments/payment/@id/data()) + 1 return $id").execute(context);
        
        if (newIdString.isEmpty()) {
            return 1l;
        }
        
        Long newId;
        
        try {
            newId = Long.parseLong(newIdString);
        }
        catch (NumberFormatException e) {
            throw new BaseXException("Unexpected data returned as id: " + newIdString, e);
        }
        
        return newId;
    }
    
    private static String paymentToXml(Payment payment, Long id)
    {   
        String paymentXml = "<payment id=\"" + id + "\">" + 
                            "<subjectId>" + payment.getSubjectID() + "</subjectId>"+
                            "<number>" + payment.getNumber() + "</number>"+
                            "<amount>" + payment.getAmount() + "</amount>"+
                            "<date>" + DATE_FORMAT.format(payment.getDate().getTime()) + "</date>" +
                            (payment.getDescription() != null ? "<description>" + payment.getDescription() + "</description>" : "") +
                            "</payment>";
        
        return paymentXml;
    }
    
    private static void validate(Payment payment)
    {
        if (payment == null) {
            throw new IllegalArgumentException("Payment must not be null");
        }
        
        if (payment.getAmount() == null) {
            throw new IllegalEntityException("Amount should not be null");
        }
        
        if (payment.getDate() == null) {
            throw new IllegalEntityException("Date should not be null");
        }
    }
    
    @Override
    public List<Payment> findAllPayments(Subject subject, Calendar dateFrom, Calendar dateTo) {
        List<Payment> list = new ArrayList<Payment>();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        
        try{
            String selectQuery = "for $payment in /prvydevw/payments/payment";
            String nextKeyword = " where";
            
            if (subject != null) {
                selectQuery += nextKeyword + " $payment/subjectId = " + subject.getId();
                nextKeyword = " and";
            }
            
            if (dateFrom != null) {
                selectQuery += nextKeyword + " xs:date($payment/date) >= xs:date('" + format.format(dateFrom.getTime()) + "')";
                nextKeyword = " and";
            }
            
            if (dateTo != null) {
                selectQuery += nextKeyword + " xs:date($payment/date) <= xs:date('" + format.format(dateTo.getTime()) + "')";
            }
            
            selectQuery += " return $payment/@id/data()";
            
            String payments = new XQuery(selectQuery).execute(context);
            
            if (payments.isEmpty()) {
                return list;
            }
            
            for (String id : payments.split(" ")) {
                list.add(getPayment(Long.valueOf(id)));
            }
        }
        catch (BaseXException ex){
            String msg = "Error when finding all payments with parameters!!";
            logger.log(Level.SEVERE, msg, ex);
            throw new ServiceFailureException(msg,ex);
        }
        
        return list;
    }
}
